事件循环
# 事件循环
JS自诞生起就是一门单线程的非阻塞的脚本语言。
参考链接:事件循环机制的那些事 (opens new window)
[TOC]
# 一、预备概念
# 1.1 函数调用形成了一个栈帧
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); // 返回 42
2
3
4
5
6
7
8
9
10
11
当调用 bar
时,创建了第一个帧 ,帧中包含了 bar
的参数和局部变量。当 bar
调用 foo
时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了 foo
的参数和局部变量。当 foo
返回时,最上层的帧就被弹出栈(剩下 bar
函数的调用帧 )。当 bar
返回的时候,栈就空了。
# 1.2 对象被分配在一个堆中
堆,即用以表示一大块非结构化的内存区域。
# 1.3 一个待处理的消息队列
一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都关联着一个用以处理这个消息的函数。
在事件循环期间的某个时刻,运行时从最先进入队列的消息开始处理队列中的消息。为此,这个消息会被移出队列,并作为输入参数调用与之关联的函数。正如前面所提到的,调用一个函数总是会为其创造一个新的栈帧。
函数的处理会一直进行到执行栈再次为空为止;然后事件循环将会处理队列中的下一个消息(如果还有的话)。
- 执行至完成
每一个消息完整的执行后,其它消息才会被执行。这为程序的分析提供了一些优秀的特性,包括:一个函数执行时,它永远不会被抢占,并且在其他代码运行之前完全运行(且可以修改此函数操作的数据)。
这个模型的一个缺点在于当一个消息需要太长时间才能处理完毕时,Web应用就无法处理用户的交互,例如点击或滚动。浏览器用“程序需要过长时间运行”的对话框来缓解这个问题。一个很好的做法是缩短消息处理,并在可能的情况下将一个消息裁剪成多个消息。
# 1.4 单线程
# 1.4.1 理解单线程
JS的单线程并不是指整个JS引擎只有1个线程,而是指运行代码只有1个线程,但是它还有其他线程来执行其他任务。比如时间函数的计时、AJAX技术中的和后台交互等操作。
所以,实际情况应该是:JS引擎中执行代码的线程开始运行代码,当执行到异步方法时,把异步的回调方法放入到队列中,然后由专门计时的线程开始计时。代码线程继续运行。如果计时的时间已到,那么它会通知代码线程来执行队列中对应的回调函数。当然前提是代码线程已经把同步代码执行完后,否则需要继续等待。
# 1.4.2 单线程的弱点
- 无法利用多核CPU。
- 错误会引起整个应用退出,应用的健壮性值的考验。
- 大量计算占用CPU导致无法继续调用异步I/O。
# 二、事件循环
- 从全局任务
script
开始,任务依次进入栈中,被主线程执行,执行完后出栈。- 遇到异步任务,交给异步处理模块处理,对应的异步处理线程处理异步任务需要的操作,例如定时器的计数和异步请求监听状态的变更。
- 当异步任务达到可执行状态时,事件触发线程将回调函数加入任务队列,等待栈为空时,依次进入栈中执行。
先微后宏。
- 由于执行代码入口都是全局任务
script
,而全局任务属于宏任务,所以当栈为空,同步任务任务执行完毕时,会先执行微任务队列里的任务。- 微任务队列里的任务全部执行完毕后,会读取宏任务队列中排最前的任务。
- 执行宏任务的过程中,遇到微任务,依次加入微任务队列。
- 栈空后,再次读取微任务队列里的任务,依次类推。
- 栈 就像是一个容器,任务都是在栈中执行。
- 主线程 就像是操作员,负责执行栈中的任务。
- 任务队列 就像是等待被加工的物品。
- 异步任务完成注册后会将回调函数加入任务队列等待主线程执行。
- 执行栈中的同步任务执行完毕后,会查看并读取任务队列中的事件函数,于是任务队列的函数结束等待状态,进入执行栈,开始执行。
之所以称之为事件循环,是因为它经常按照类似如下的方式来被实现:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
2
3
如果当前没有任何消息,queue.waitForMessage()
会同步地等待消息到达。
- 用户交互、IO 和定时器会向事件队列中加入事件。
- 并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时 在运行(尽管在任意时刻只处理一个事件)。
# 2.1 setTimeout
- 函数
setTimeout
接受两个参数:待加入队列的消息和一个延迟(可选,默认为 0)。 - 这个延迟代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其它消息,在这段延迟时间过去之后,消息会被马上处理。
- 但是,如果有其它消息,
setTimeout
消息必须等待其它消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间。
const foo = () => console.log("First");
const bar = () => setTimeout(() => console.log("Second"));
const baz = () => console.log("Third");
bar();
foo();
baz();
// First Third Second
2
3
4
5
6
7
8
# 2.2 常见异步操作
Ajax
DOM的事件操作
setTimeout
Promise的then方法
Node的读取文件
# 2.3 任务
采纳 JSC 引擎的术语,我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。
# 2.3.1 宏任务 (macrotask)
script(全局任务)
, setTimeout
, setInterval
, setImmediate
, I/O
, UI rendering
,script(整体代码)
# 2.3.2 微任务(microtask)
process.nextTick
, Promise.then()
, Object.observe
, MutationObserver
,Promise
在微任务中 process.nextTick 优先级高于Promise
- 如果是宏任务,则新增一个宏任务队列,任务队列中的宏任务可以有多个来源。
- 如果是微任务,则直接压入微任务队列。
Event loop过程:将一个macro-task执行并出队;将一队micro-task执行并出队;执行渲染操作,更新界面;处理worker相关的任务。
渲染时机:在异步任务中实现DOM修改时,应该将其包装成micro任务。
// task 是一个用于修改DOM的回调
setTimeout(task, 0)
// 因为script是一个macro任务,所以执行完script就要去处理micro队列,接着就是render;然后本次render并没有执行task,所以没有渲染DOM。
Promise.resolve().then(task)
// 不用再等待一轮事件循环,就可以为用户呈现最及时的渲染结果。
2
3
4
5
6